/// <reference path="../define/IChannel.d.ts" />

import { EventHandlerTarget } from "./EventHandlerTarget";
import { ProtobufHelper } from "./ProtobufHelper";

/**
 * 将IMSession进一步封装为连接客户端,走事件
 *
 * @export
 * @class Client
 * @extends {EventHandlerTarget}
 */
export class Client extends EventHandlerTarget {


    //当前连接的id,为了在各异步事件中校验当前操作是否还在同个连接中
    //因为存在旧的连接关闭时,对应的连接事件可能会异步触发,而新的连接已经建立,这个时候旧的连接事件就不需要处理了
    private currChannelId: number = 0;
    //下面是rpc消息的处理成员
    private rpcId: number = 0;
    private resCallback: Map<string, (connectErr: string | null, resOp: number, resMsg: any) => void> = new Map<string, (connectErr: string | null, resOp: number, resMsg: any) => void>();
    private resCallbackOpcode: Map<string, number | null> = new Map<string, number | null>();
    private resCallbackTimeout: Map<string, any> = new Map<string, any>();

    private protobufHelper: ProtobufHelper;
    private connectChannelFactory: () => IChannel;

    /**
     *连接对象
     *
     * @type {(Readonly<IChannel | null>)}
     * @memberof Client
     */
    public channel: Readonly<IChannel | null> = null;
    /**
     *当前是否连接
     *
     * @type {boolean}
     * @memberof Client
     */
    public connected: Readonly<boolean> = false;
    /**
     *当前是否连接并认证通过
     *
     * @type {boolean}
     * @memberof Client
     */
    public authed: Readonly<boolean> = false;


    //#region 私有方法
    /** 强制断开连接并重置和清理相关数据 */
    private abortChannel() {
        if (!this.currChannelId || !this.connected) return;

        this.currChannelId = 0;
        this.connected = false;
        this.authed = false;

        this.resCallbackTimeout.forEach((hd, key) => {
            clearTimeout(hd);
        });
        this.resCallback.forEach((cb, key) => {
            cb("cancel", 0, null);
        });
        this.resCallback.clear();
        this.resCallbackOpcode.clear();
        this.resCallbackTimeout.clear();

        if (this.channel) {
            try {
                this.channel.close(50000, "强制断开");
            } catch (e) { }
            this.channel = null;
        }
    }
    private onCloseProc(code: number, reason: string) {
        if (code >= 1000 && code < 10000) {
            //这个错误码范围是系统/内核层,直接指定错误消息好了
            reason = "服务器断开";
        }
        this.abortChannel();//先清理数据,再触发回调
        //触发通用注册事件
        this.triggerClose(code, reason);
    }
    private onErrorProc(errMsg: string) {
        this.abortChannel();//先清理数据,再触发回调
        //触发通用注册事件
        this.triggerClose(1, errMsg);
    }
    private onMessageProc(op: number, msg: any, reply: ((resultOpcode: number, result: any) => void) | null = null, remoteInfo: any = null) {
        //触发通用注册事件
        this.triggerMessage(op, msg, reply, remoteInfo);
    }

    //#endregion

    /**
     * Creates an instance of IMSession.
     * @param {ProtobufHelper} protobufHelper
     * @param {()=>IChannel} connectChannelFactory 开始连接并返回连接对象
     * @memberof IMSession
     */
    constructor(protobufHelper: ProtobufHelper, connectChannelFactory: () => IChannel) {
        super();

        if (protobufHelper == null) throw "protobufHelper 不可空";
        if (connectChannelFactory == null) throw "connectChannelFactory 不可空";

        this.protobufHelper = protobufHelper;
        this.connectChannelFactory = connectChannelFactory;
    }


    /**
     *连接服务器,并由外部完成认证,要求回传认证结果
     *
     * @param {(errMsg: string, authCallback: (connected: boolean) => void) => void} callback 完成连接,成功则errMsg=null,并且要求回传认证结果(authCallback),失败则只有errMsg
     * @memberof Client
     */
    public connect(callback: (errMsg: string | null, authCallback: (authed: boolean) => void) => void): void {
        this.abortChannel();
        this.currChannelId = Date.now();//每次连接,都使用新的客户端id,让旧连接(如果存在)的所有事件都忽略
        var innerClientId = this.currChannelId;
        this.channel = this.connectChannelFactory();
        this.channel.onOpen((ev) => {
            //连接成功事件
            if (innerClientId != this.currChannelId) return;
            this.connected = true;
            console.debug("连接上服务器(等待认证)");
            if (callback) {
                callback(null, (authed) => {
                    this.authed = authed;
                    if (!authed) {
                        //认证没通过则直接关闭连接
                        this.abortChannel();
                    }
                });
            }
        });
        this.channel.onClose((code, reason) => {
            //连接关闭事件
            if (innerClientId != this.currChannelId) return;

            if (!this.connected) {
                //服务器已经关闭了直接返回(在其他流程手动做了这个操作)
                return;
            }

            if (code == 4001) {
                //特殊定义需要处理转换
                var pas = (reason || "").split('|');
                if (pas.length >= 2) {
                    code = parseInt(pas[0]);
                    reason = pas[1];
                }
            }

            this.connected = false;
            console.debug("断开服务器:code:" + code + "|reason:" + reason);
            if (!this.authed) {
                //如果没认证通过就关闭会话了.需要触发本次的连接回调
                this.abortChannel();//先清理数据,再触发回调
                if (callback) {
                    callback(reason || "服务器断开", () => { });
                }
            } else {
                //认证通过后才关闭的连接,则正常走连接关闭事件
                this.onCloseProc(code, reason);
            }
        });
        this.channel.onError((errMsg) => {
            //连接错误事件
            if (innerClientId != this.currChannelId) return;
            //连接不上(如服务器没开等问题),服务器异常断开等情况会走错误事件
            console.debug("服务器连接错误:" + errMsg);
            if (!this.authed) {
                //如果没认证通过就关闭会话了.需要触发本次的连接回调
                this.abortChannel();//先清理数据,再触发回调
                if (callback) {
                    callback(errMsg || "异常断开", () => { });
                }
            } else {
                //认证通过后才出错,则正常走连接错误事件
                this.onErrorProc(errMsg);
            }
        });
        this.channel.onMessage((data: ArrayBuffer, remoteInfo: any) => {
            if (innerClientId != this.currChannelId) return;

            var opArr = new Uint8Array(data.slice(0, 2));//前两个字节是存放本消息的类型码
            var op = opArr[0] + (opArr[1] << 8);
            var bufLenArr = new Uint8Array(data.slice(2, 6));//4个是后面消息体长度
            var bufLen = bufLenArr[0] + (bufLenArr[1] << 8) + (bufLenArr[2] << 16) + (bufLenArr[3] << 24);//只不过websocket不需要处理粘包,所以算出来了这个字段却没用
            var buf = new Uint8Array(data.slice(6, 6 + bufLen));

            var msg = this.protobufHelper!.decode(op, buf);
            if (!msg || !op) {
                console.error("onMessage.Error,op:" + op + ",msg:", msg);
                return;
            }
            var msgName = this.protobufHelper.getMsgName(op);
            console.debug("onMessage[" + msgName + "]", msg);

            var rpcId = msg["RpcId"];
            var reply: ((resultOpcode: number, result: any) => void) | null = null;
            if (rpcId) {
                //有RpcId,要么是回复,要么是服务端的Rpc消息
                var rpcCBKey = 'RpcId_' + rpcId
                var replyOpcode = this.resCallbackOpcode.get(rpcCBKey);
                if (replyOpcode && replyOpcode == op) {
                    var cb = this.resCallback.get(rpcCBKey);
                    //防止和服务端发出的rpc消息混在一起,还需要匹配是对应回调的消息类型码
                    if (cb) cb(null, op, msg);
                    return;
                }
                //没匹配到,说明是服务端下发的Rpc消息,需要客户端回复
                reply = (resultOpcode, result) => {
                    result["RpcId"] = rpcId;
                    this.sendRpc(resultOpcode, result);
                };
            }

            //服务主动下发消息
            this.onMessageProc(op, msg, reply);
        });
    }


    /**
     *断开连接,并且不触发事件
     *
     * @memberof Client
     */
    public disconnect() {
        this.abortChannel();
    }


    /**
     * 发送协议Rpc消息,并返回是否发送成功,当前连接已经断开或者消息序列化失败等都会发送失败
     *
     * @param {number} opcode 消息类型码
     * @param {*} msg 消息对象,需要对应的protobuf类型实例
     * @param {number|null} replyOpcode 回复消息的类型码
     * @param {(((connectErr:string,reOpCode: number, msg: ResultT|null) => void) | null)} [onReply=null] 当消息需要回复时必传 (connectErr为通讯错误,如"timeout","cancel")
     * @param {number} rpcWaitTimeoutMS 如果是rpc消息,则可设置等待的超时毫秒数,超时则触发onReply("timeout", 0, null)
     * @memberof Session
     */
    public sendRpc<RequestT extends { RpcId?: number|Long|null }, ResponseT extends { RpcId?: number|Long|null, Error?: number|Long|null, Message?: string|null }>(
        opcode: number, msg: RequestT,
        replyOpcode: number | null = null,
        onReply: ((connectErr: string | null, reOpCode: number, msg: ResponseT) => void) | null = null,
        rpcWaitTimeoutMS: number = 60000): boolean {
        if (!this.connected || !this.channel) {
            //服务器已经关闭了直接返回
            return false;
        }

        var msgName = this.protobufHelper.getMsgName(opcode);
        console.debug("send[" + msgName + "]", msg);

        if (onReply) {
            //有要求回复
            msg.RpcId = ++this.rpcId;
            var rpcCBKey = 'RpcId_' + msg.RpcId;
            this.resCallbackOpcode.set(rpcCBKey, replyOpcode);
            this.resCallback.set(rpcCBKey, (connectErr, resOp, resMsg) => {
                clearTimeout(this.resCallbackTimeout.get(rpcCBKey));
                this.resCallbackOpcode.delete(rpcCBKey);
                this.resCallback.delete(rpcCBKey);
                this.resCallbackTimeout.delete(rpcCBKey);
                onReply(connectErr, resOp, resMsg);
            });
            this.resCallbackTimeout.set(rpcCBKey, setTimeout(() => {
                console.warn("请求超时(等待" + rpcWaitTimeoutMS + "ms未响应),opcode:" + opcode + "[" + this.protobufHelper.getMsgName(opcode) + "],msg:" + JSON.stringify(msg) + "");
                this.resCallbackOpcode.delete(rpcCBKey);
                this.resCallback.delete(rpcCBKey);
                this.resCallbackTimeout.delete(rpcCBKey);
                onReply("timeout", 0, {} as ResponseT);
            }, rpcWaitTimeoutMS));
        }
        var buf = this.protobufHelper.encode(opcode, msg);
        if (buf == null) {
            console.error("opcode:[" + opcode + "],msg:", msg, "不能正确序列化为二进制,请确定protobufHelper是否配置正确!");
            return false;
        }
        var sendBuf = new Uint8Array(buf.byteLength + 6);
        sendBuf.set([opcode & 0xff, (opcode & 0xff00) >> 8], 0);
        sendBuf.set([buf.byteLength & 0xff, (buf.byteLength & 0xff00) >> 8, (buf.byteLength & 0xff0000) >> 16, (buf.byteLength & 0xff000000) >> 24], 2);
        sendBuf.set(buf, 6);
        this.channel.send(sendBuf.buffer);
        return true;
    }
    /**
     *发送不需要回调的协议消息,并返回是否发送成功,当前连接已经断开或者消息序列化失败等都会发送失败
     *
     * @param {number} msgTypeCode 消息类型码
     * @param {*} msg 消息对象,需要对应的protobuf类型实例
     * @memberof Client
     */
    public sendMsg(msgTypeCode: number, msg: any): boolean {
        return this.sendRpc(msgTypeCode, msg);
    }

    /**
     *自动重发Rpc消息,3秒没回调将会自动重发!
     *
     * @template MsgT
     * @template ResultT
     * @param {number} opcode
     * @param {MsgT} req
     * @param {number} resultOpcode
     * @param {(((errMsg: string | null, result: ResultT | null) => void) | null)} callback
     * @param {(msg:string)=>void} showLoading 需要显示加载中时会触发
     * @param {()=>void} hideLoading 需要隐藏加载中时会触发
     * @return {*}  {void}
     * @memberof Client
     */
    public sendRpcAutoRetry<MsgT extends { RpcId?: number }, ResultT extends { RpcId?: number, Error?: number, Message?: string }>(
        opcode: number, req: MsgT, resultOpcode: number,
        callback: ((errMsg: string | null, result: ResultT | null) => void) | null,
        showLoading: (msg: string) => void,
        hideLoading: () => void
    ): void {
        if (!this.connected) {
            if (callback) callback("未连接", null);
            return;
        }
        //准备超过500毫秒就显示loading(服务器繁忙,请稍候),并记录日志
        var onceSendTimeoutMS = 3000;
        var isShowLoading = false;
        var showLoadingTmp = () => {
            isShowLoading = true;
            showLoading("服务器繁忙, 请稍候...");
        };
        //正常请求响应很快,不需要显示,超过1秒还没响应才显示
        var waitShowLoadingHD = setTimeout(showLoadingTmp, 1000);

        var sendFn = () => {
            if (!this.connected || !this.channel) {
                clearTimeout(waitShowLoadingHD);
                if (isShowLoading) {
                    hideLoading();
                }
                if (callback) callback("未连接", null);
                return;
            }
            this.sendRpc<MsgT, ResultT>(opcode, req, resultOpcode, (errMsg, op, msg) => {
                if (errMsg == "timeout") {
                    //超时未收到回调,则重发,不走回调
                    console.debug("发送Rpc:[" + opcode + "],回调超时(" + onceSendTimeoutMS + "ms),重发!", req, new Date().toLocaleString());
                    setTimeout(sendFn, 1);
                    return;
                }
                //其他则需要完成回调了,完成回调前先把loading去掉
                clearTimeout(waitShowLoadingHD);
                if (isShowLoading) {
                    hideLoading();
                }

                if (errMsg) {
                    if (callback) callback(errMsg, null);
                    return;
                }
                if (msg && msg.Error) {
                    if (callback) callback(msg.Message ?? "", null);
                    return;
                }
                if (callback) callback(null, msg);
            }, onceSendTimeoutMS);
        };

        sendFn();
    }



    //#region  events

    /**
     *当连接被关闭时触发,只在完成连接(认证成功)后触发,并且手动断开连接不触发
     *
     * @param {(client: Client, code: number, reason: string) => void} callback 如果是连接异常关闭则code=1,reason为具体错误消息
     * @param {object} target
     * @memberof Client
     */
    public onClose(callback: (client: Client, code: number, reason: string) => void, target: object) {
        this.on("Close", callback, target);
    }
    public offClose(callback: (client: Client, code: number, reason: string) => void, target: object) {
        this.off("Close", callback, target);
    }
    private triggerClose(code: number, reason: string) {
        this.emit("Close", this, code, reason);
    }

    /**
     *当收到消息时触发
     *
     * @param {((client: Client, opcode: number, msg: any, reply: ((resultOpcode: number, result: any) => void) | null, remoteInfo: any) => void)} callback 如果收到的是需要响应的消息,需要自行调用reply进行回复,remoteInfo为udp类的协议时使用
     * @param {object} target
     * @memberof Client
     */
    public onMessage(callback: (client: Client, opcode: number, msg: any, reply: ((resultOpcode: number, result: any) => void) | null, remoteInfo: any) => void, target: object) {
        this.on("Message", callback, target);
    }
    public offMessage(callback: (client: Client, opcode: number, msg: any, reply: ((resultOpcode: number, result: any) => void) | null, remoteInfo: any) => void, target: object) {
        this.off("Message", callback, target);
    }
    private triggerMessage(opcode: number, msg: any, reply: ((resultOpcode: number, result: any) => void) | null = null, remoteInfo: any = null) {
        this.emit("Message", this, opcode, msg, reply, remoteInfo);
    }
    //#endregion
}
